import { mat4, vec3, quat } from '../gl-matrix/index.js'
import { eventEmitter } from '../util/eventEmitter.js';

const DEFAULT_TRANSLATION = new Float32Array([0, 0, 0]);
const DEFAULT_ROTATION = new Float32Array([0, 0, 0, 1]);
const DEFAULT_SCALE = new Float32Array([1, 1, 1]);

export class Node {
    constructor() {
        this.name = null;
        this.children = [];
        this.parent = null;

        this.selectable = false;
        this.visible = true;
        this.boxCollider = null;

        this._renderer = null;
        this._renderObject = null;

        this._dirtyTRS = false;
        this._translation = null;
        this._rotation = null;
        this._scale = null;

        this._matrix = null;
        this._dirtyWorldMatrix = false;
        this._worldMatrix = null;
        this._worldMatrixInvert = null;
    }

    get renderer() {
        return this._renderer;
    }

    set renderer(value) {
        if (this._renderer == value) {
            return;
        }

        this._renderer = value;

        if (value) {
            this._rendererChanged(value);

            for (let child of this.children) {
                child.renderer = value;
            }
        }
    }

    get worldMatrix() {
        if (!this._worldMatrix) {
            this._dirtyWorldMatrix = true;
            this._worldMatrix = mat4.create();
        }

        if (this._dirtyWorldMatrix || this._dirtyTRS) {
            if (this.parent) {
                mat4.mul(this._worldMatrix, this.parent.worldMatrix, this._updateLocalMatrix());
            } else {
                mat4.copy(this._worldMatrix, this._updateLocalMatrix());
            }
            this._dirtyWorldMatrix = false;
        }

        return this._worldMatrix;
    }

    set translation(value) {
        if (value != null) {
            this._dirtyTRS = true;
            this.setMatrixDirty();
        }
        this._translation = value;
    }

    get translation() {
        this._dirtyTRS = true;
        this.setMatrixDirty();
        if (!this._translation) {
            this._translation = vec3.clone(DEFAULT_TRANSLATION);
        }
        return this._translation;
    }

    set rotation(value) {
        if (value != null) {
            this._dirtyTRS = true;
            this.setMatrixDirty();
        }
        this._rotation = value;
    }

    get rotation() {
        this._dirtyTRS = true;
        this.setMatrixDirty();
        if (!this._rotation) {
            this._rotation = quat.clone(DEFAULT_ROTATION);
        }
        return this._rotation;
    }

    set scale(value) {
        if (value != null) {
            this._dirtyTRS = true;
            this.setMatrixDirty();
        }
        this._scale = value;
    }

    get scale() {
        this._dirtyTRS = true;
        this.setMatrixDirty();
        if (!this._scale) {
            this._scale = vec3.clone(DEFAULT_SCALE);
        }
        return this._scale;
    }

    add(value) {
        if (!value || value.parent == this) {
            return;
        }

        if (value.parent) {
            value.parent.remove(value);
        }

        value.parent = this;

        this.children.push(value);

        if (this._renderer) {
            value.renderer = this._renderer;
        }
    }

    remove(value) {
        let i = this.children.indexOf(value);
        if (i > -1) {
            this.children.splice(i, 1);
            value.parent = null;
        }
    }

    setMatrixDirty() {
        if (!this._dirtyWorldMatrix) {
            this._dirtyWorldMatrix = true;
            for (let child of this.children) {
                child.setMatrixDirty();
            }
        }
    }

    _updateLocalMatrix() {
        if (!this._matrix) {
            this._matrix = mat4.create();
        }

        if (this._dirtyTRS) {
            this._dirtyTRS = false;
            mat4.fromRotationTranslationScale(
                this._matrix,
                this._rotation || DEFAULT_ROTATION,
                this._translation || DEFAULT_TRANSLATION,
                this._scale || DEFAULT_SCALE);
        }

        return this._matrix;
    }

    _update(timestamp, frameDelta) {
        if (this.boxCollider) {
            this.boxCollider.setWorldBox(this.worldMatrix);
        }

        this.onUpdate(timestamp, frameDelta);

        for (let child of this.children) {
            child._update(timestamp, frameDelta);
        }
    }

    onUpdate(timestamp, frameDelta) {

    }

    _hitTestSelectableNode(ray) {
        if (this.boxCollider) {
            if (this._worldMatrixInvert == null) {
                this._worldMatrixInvert = mat4.create();
            }

            mat4.invert(this._worldMatrixInvert, this.worldMatrix);
            let localRay = ray.createNewByMat4(this._worldMatrixInvert);
            let intersection = localRay.intersectsAABB(this.boxCollider.min, this.boxCollider.max);

            if (intersection) {
                vec3.transformMat4(intersection, intersection, this.worldMatrix);
                return intersection;
            }
        }

        for (let child of this.children) {
            let intersection = child._hitTestSelectableNode(ray);
            if (intersection) {
                return intersection;
            }
        }
        return null;
    }

    hitTest(ray) {
        if (this.selectable && this.visible) {
            let intersection = this._hitTestSelectableNode(ray);

            if (intersection) {
                return {
                    node: this,
                    intersection: intersection,
                    distance: vec3.distance(ray.origin, intersection),
                };
            }
            return null;
        }

        let result = null;

        for (let child of this.children) {
            let childResult = child.hitTest(ray);
            if (childResult) {
                if (!result || result.distance > childResult.distance) {
                    result = childResult;
                }
            }
        }

        return result;
    }

    _rendererChanged(renderer) {
        if (this.boxCollider) {
            renderer._colliders.push(this);
        }

        this.onRendererChanged(renderer);
    }

    onRendererChanged(renderer) {

    }

    setVisible(value) {
        this.visible = value;

        for (let child of this.children) {
            child.setVisible(value);
        }
    }
}

eventEmitter(Node);
